<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<title>TTS分拣</title>
<style>
body{margin:0;overflow:hidden;font-family:Arial;background:#111;color:#fff;}
#info{position:absolute;top:10px;left:10px;background:rgba(0,0,0,.6);padding:8px 12px;border-radius:4px;}
</style>
</head>
<body>
<div id="info">
  <button id="btnPlay">暂停</button>
  <button id="btnSpeed">x1</button>
  <span id="stats">待分拣货物: 0</span>
</div>

<script src="ThreeJs/js/three.js"></script>
<script src="ThreeJs/js/OrbitControls.js"></script>

<script type="module">
/* ---------- 基础场景 ---------- */
const scene    = new THREE.Scene();
scene.background = new THREE.Color(0x202020);
const camera   = new THREE.PerspectiveCamera(60, innerWidth/innerHeight, 0.1, 1000);
camera.position.set(0, 15, 20);
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
const ctrl = new THREE.OrbitControls(camera, renderer.domElement);
ctrl.enableDamping = true;

scene.add(new THREE.AmbientLight(0xffffff, 0.6));
const dir = new THREE.DirectionalLight(0xffffff, 1);
dir.position.set(5,10,7); dir.castShadow = true;
scene.add(dir);

const ground = new THREE.Mesh(
    new THREE.PlaneGeometry(50,50),
    new THREE.MeshStandardMaterial({color:0x333333})
);
ground.rotation.x = -Math.PI/2; ground.receiveShadow = true;
//scene.add(ground);

/* ---------- 环带几何参数 ---------- */
const L = 40, W = 6, beltW = 1;
const halfL = L/2, halfW = W/2;

/* 环带网格 */
const shape = new THREE.Shape();
shape.moveTo(-halfL, -halfW);
shape.lineTo( halfL, -halfW);
shape.lineTo( halfL,  halfW);
shape.lineTo(-halfL,  halfW);
shape.lineTo(-halfL, -halfW);
const hole = new THREE.Path();
hole.moveTo(-halfL+beltW, -halfW+beltW);
hole.lineTo( halfL-beltW, -halfW+beltW);
hole.lineTo( halfL-beltW,  halfW-beltW);
hole.lineTo(-halfL+beltW,  halfW-beltW);
hole.lineTo(-halfL+beltW, -halfW+beltW);
shape.holes.push(hole);
const beltMesh = new THREE.Mesh(
    new THREE.ExtrudeGeometry(shape, {depth:0.1, bevelEnabled:false}),
    new THREE.MeshStandardMaterial({color:0x666666})
);
beltMesh.rotation.x = -Math.PI/2;
beltMesh.position.y = 0.05;
beltMesh.receiveShadow = true;
scene.add(beltMesh);

/* 显示坐标轴（红 X、绿 Y、蓝 Z） */
const axes = new THREE.AxesHelper(10);
//scene.add(axes);

/* ---------- 分拣出口 ---------- */
const N = 33;
//const colors = [0xff0000, 0x00ff00, 0x0000ff,0xffaa00, 0x00ffaa, 0xaa00ff];
const colors = [];
for (let i = 0; i < N; i++) {
    const hue = i / N;
	//console.log(hue);
    colors.push(new THREE.Color().setHSL(hue, 1, 0.5));
}
//const n2 = (N-1) >> 1;
//const chutes = [];
//for (let i = 1; i <=N/2; i++) {
//    chutes.push({name:'T-'+i,  side:'z', pos:[-n2 + (i-1)*2, 4], dir:new THREE.Vector3(0,0,1)});
//}
//for (let i = 1; i <=N/2; i++) {
//    chutes.push({name:'B-'+i,  side:'-z', pos:[-n2 + (i-1)*2, -4], dir:new THREE.Vector3(0,0,-1)});
//}
//console.log(chutes);
//console.log(JSON.stringify(chutes));

/* 分拣出口 side:xyz-x-y-z 相对tts的位置:x左-x右y上-y下z前-z后 */
const chutes = [
    {name:'B-1',  side:'z',  pos:[-18, 4], dir:new THREE.Vector3(0,-1,1)},
    {name:'B-1',  side:'z',  pos:[-16, 4], dir:new THREE.Vector3(0,-1,1)},
    {name:'B-1',  side:'z',  pos:[-14, 4], dir:new THREE.Vector3(0,-1,1)},
    {name:'B-1',  side:'z',  pos:[-12, 4], dir:new THREE.Vector3(0,-1,1)},
    {name:'B-1',  side:'z',  pos:[-10, 4], dir:new THREE.Vector3(0,-1,1)},
    {name:'B-2',  side:'z',  pos:[- 8, 4], dir:new THREE.Vector3(0,-1,1)},
    {name:'B-3',  side:'z',  pos:[- 6, 4], dir:new THREE.Vector3(0,-1,1)},
    {name:'B-4',  side:'z',  pos:[- 4, 4], dir:new THREE.Vector3(0,-1,1)},
    {name:'B-5',  side:'z',  pos:[- 2, 4], dir:new THREE.Vector3(0,-1,1)},
    {name:'B-6',  side:'z',  pos:[  0, 4], dir:new THREE.Vector3(0,-1,1)},
    {name:'B-7',  side:'z',  pos:[  2, 4], dir:new THREE.Vector3(0,-1,1)},
    {name:'B-8',  side:'z',  pos:[  4, 4], dir:new THREE.Vector3(0,-1,1)},
    {name:'B-9',  side:'z',  pos:[  6, 4], dir:new THREE.Vector3(0,-1,1)},
    {name:'B-10', side:'z',  pos:[  8, 4], dir:new THREE.Vector3(0,-1,1)},
    {name:'B-11', side:'z',  pos:[ 10, 4], dir:new THREE.Vector3(0,-1,1)},
    {name:'B-11', side:'z',  pos:[ 12, 4], dir:new THREE.Vector3(0,-1,1)},
    {name:'B-11', side:'z',  pos:[ 14, 4], dir:new THREE.Vector3(0,-1,1)},
    {name:'B-11', side:'z',  pos:[ 16, 4], dir:new THREE.Vector3(0,-1,1)},
    {name:'B-11', side:'z',  pos:[ 18, 4], dir:new THREE.Vector3(0,-1,1)},
    {name:'T-1',  side:'-z', pos:[18,-4], dir:new THREE.Vector3(0,-1,-1)},
    {name:'T-1',  side:'-z', pos:[16,-4], dir:new THREE.Vector3(0,-1,-1)},
    {name:'T-1',  side:'-z', pos:[14,-4], dir:new THREE.Vector3(0,-1,-1)},
    {name:'T-1',  side:'-z', pos:[12,-4], dir:new THREE.Vector3(0,-1,-1)},
    {name:'T-1',  side:'-z', pos:[10,-4], dir:new THREE.Vector3(0,-1,-1)},
    {name:'T-2',  side:'-z', pos:[ 8,-4], dir:new THREE.Vector3(0,-1,-1)},
    {name:'T-3',  side:'-z', pos:[ 6,-4], dir:new THREE.Vector3(0,-1,-1)},
//    {name:'T-4',  side:'-z', pos:[4, -4], dir:new THREE.Vector3(0,0,-1)},
//    {name:'T-5',  side:'-z', pos:[2, -4], dir:new THREE.Vector3(0,0,-1)},
//    {name:'T-6',  side:'-z', pos:[  0, -4], dir:new THREE.Vector3(0,0,-1)},
//    {name:'T-7',  side:'-z', pos:[- 2, -4], dir:new THREE.Vector3(0,0,-1)},
//    {name:'T-8',  side:'-z', pos:[- 4, -4], dir:new THREE.Vector3(0,0,-1)},
    {name:'T-9',  side:'-z', pos:[- 6,-4], dir:new THREE.Vector3(0,-1,-1)},
    {name:'T-10', side:'-z', pos:[- 8,-4], dir:new THREE.Vector3(0,-1,-1)},
    {name:'T-11', side:'-z', pos:[-10,-4], dir:new THREE.Vector3(0,-1,-1)},
    {name:'T-11', side:'-z', pos:[-12,-4], dir:new THREE.Vector3(0,-1,-1)},
    {name:'T-11', side:'-z', pos:[-14,-4], dir:new THREE.Vector3(0,-1,-1)},
    {name:'T-11', side:'-z', pos:[-16,-4], dir:new THREE.Vector3(0,-1,-1)},
    {name:'T-11', side:'-z', pos:[-18,-4], dir:new THREE.Vector3(0,-1,-1)}
];
chutes.forEach((o,idx)=>{
    const len = 2;
    const b = new THREE.BoxGeometry(beltW, 0.1, len);
    const m = new THREE.MeshStandardMaterial({color:colors[idx]});
    const mesh = new THREE.Mesh(b, m);
    if(o.side==='-z') {
        mesh.position.set(o.pos[0], -0.6, o.pos[1]+0.3);
        mesh.rotation.x = -Math.PI / 4;
    }
    if(o.side==='z') {
        mesh.position.set(o.pos[0], -0.6, o.pos[1]-0.3);
        mesh.rotation.x = Math.PI / 4;
    }
    mesh.receiveShadow = mesh.castShadow = false;
    scene.add(mesh);
});

/* 导入口输送段 side:lrud 相对tts的位置:左右上下 */
const beltImp = [
//    {name:'IMP-RD-1', side:'rd', pos:[-2, 4], dir:new THREE.Vector3(-1,0,-1)},
//    {name:'IMP-RD-2', side:'rd', pos:[ 0, 4], dir:new THREE.Vector3(-1,0,-1)},
//    {name:'IMP-RD-3', side:'rd', pos:[ 2, 4], dir:new THREE.Vector3(-1,0,-1)},
    {name:'IMP-LU-1', side:'lu', pos:[-2, -4], dir:new THREE.Vector3(1,0,1)},
    {name:'IMP-LU-2', side:'lu', pos:[ 0, -4], dir:new THREE.Vector3(1,0,1)},
    {name:'IMP-LU-3', side:'lu', pos:[ 2, -4], dir:new THREE.Vector3(1,0,1)}
];
beltImp.forEach((o,idx)=>{
    const len = 4;
    const b = new THREE.BoxGeometry(beltW, 0.1, len);
    const m = new THREE.MeshStandardMaterial({color:0x666666});
    const mesh = new THREE.Mesh(b, m);
    mesh.position.set(o.pos[0], 0.1, o.pos[1]);
    mesh.rotation.y = Math.PI / 4;
    mesh.receiveShadow = mesh.castShadow = false;
    scene.add(mesh);
});

/* ---------- 环带路径函数 ---------- */
const totalLen = 2*(L+W);
function getPos(t){
    const d = (t % 1) * totalLen;
    if(d < L-1){
        return new THREE.Vector3(-halfL + d+0.5, 0, -halfW+0.5);           // 下边 视图是上边/后面,z负值
    }else if(d < L+W-2){
        return new THREE.Vector3( halfL-0.5, 0, -halfW + (d-L)+1.5);       // 右边
    }else if(d < 2*L+W-3){
        return new THREE.Vector3( halfL - (d-L-W)-2.5, 0,  halfW-0.5);     // 上边 视图是下边/前面,z正值
    }else{
        return new THREE.Vector3(-halfL+0.5, 0,  halfW - (d-2*L-W)-3.5);   // 左边
    }
}

/* ---------- 统一包裹类 ---------- */
class Parcel {
    constructor(id, targetIdx, impIdx){
        this.id = id;
        this.target = targetIdx;		   // 终点:目标出口
        this.impIdx = impIdx;              // 起点:导入口
        this.area = "import";              // import | tts | chute
		this.waiting = false;              // 新增：是否在等待进入分拣环

        const mat = new THREE.MeshStandardMaterial({color:colors[targetIdx]});
        this.mesh = new THREE.Mesh(new THREE.BoxGeometry(0.6,0.6,0.6), mat);
        this.mesh.castShadow = false;

        /* 初始放在导入口起点 */
        const o = beltImp[impIdx];
		this.mesh.position.set(o.pos[0]-1.5*o.dir.x, 0.4, o.pos[1]-1.5*o.dir.z);
		//if (o.side === 'lu') this.mesh.position.set(o.pos[0]-1.5, 0.4, o.pos[1]-1.5);
		//else if (o.side === 'rd') this.mesh.position.set(o.pos[0]-1.5*o.dir.x, 0.4, o.pos[1]-1.5*o.dir.z);
        
        scene.add(this.mesh);
    }
	
	/* 检查分拣环入口是否有冲突包裹 */
    checkConflict() {
        const o = beltImp[this.impIdx];
        let entranceT;

        // 计算当前导入口对应的分拣环入口位置和t值
        if (o.side === 'lu') {
            entranceT = (this.mesh.position.x + halfL - 0.5) / totalLen;
        } else if (o.side === 'rd') {
            entranceT = (-this.mesh.position.x + halfL + L + W - 2.5) / totalLen;
        }

        // 检查所有在分拣环上的包裹是否会在入口处冲突
        for (const [id, parcel] of parcels) {
            if (parcel.id === this.id || parcel.area !== 'tts') continue;

            // 计算其他包裹到达入口的时间差
            const deltaT = Math.abs(parcel.t - entranceT);
            const normalizedDelta = Math.min(deltaT, 1 - deltaT); // 考虑环形的最小差值
            
            // 如果时间差小于安全阈值（约0.5秒的行程），视为冲突
            if (normalizedDelta < 0.5 / totalLen * speed) {
                return true;
            }
        }
        return false;
    }

    /* 在导入口输送段上的移动 */
    updateImport(delta){
		const o = beltImp[this.impIdx];
		
		if (this.checkConflict()) {
			this.waiting = true; // 有冲突则等待
			return;
		} else {
			this.waiting = false; // 无冲突则继续移动
		}
		// 只有不在等待状态时才移动
		if (this.waiting) return;
		
        this.mesh.position.addScaledVector(o.dir, delta * speed*1.5);
        /* 当 z 到达环带下边 z = -halfW+0.5 时切换区域 */
        if (o.side === 'lu' && this.mesh.position.z >= -halfW + 0.5){
            this.area = "tts";
            /* 把位置对齐到环带入口 */
            this.mesh.position.set(this.mesh.position.x, 0.4, -halfW + 0.5);
            /* 计算环带上对应的 t */
            this.t = (this.mesh.position.x + halfL - 0.5) / totalLen;
        }else if (o.side === 'rd' && this.mesh.position.z < halfW - 0.5){
            this.area = "tts";
			//console.log(this.mesh.position.x);
            this.mesh.position.set(this.mesh.position.x, 0.4, halfW - 0.5);
            this.t = (-this.mesh.position.x + halfL + L + W - 2.5) / totalLen;
        }
    }

    /* 在环带上的移动 */
    updateTTS(delta){
        this.t += 0.5 * delta * speed / totalLen;
        this.t %= 1;
        const d = (this.t % 1) * totalLen;
        if(d >= totalLen-4) this.t = 0;               // 移动一圈多走4个单位
        const pos = getPos(this.t);
        this.mesh.position.copy(pos).setY(0.4);

        /* 出口检测 */
        const o = chutes[this.target];
        const eps = 0.12;
        let hit = false;
        if(o.side==='-z' && Math.abs(pos.z + (halfW-0.5)) < eps && Math.abs(pos.x - o.pos[0]) < eps) hit=true;
        if(o.side==='z'  && Math.abs(pos.z - (halfW-0.5)) < eps && Math.abs(pos.x - o.pos[0]) < eps) hit=true;
        if(hit){
            this.area = "chute";
            this.dir = o.dir.clone();
            this.mesh.position.y = 0.5;
			
			if(o.side==='-z') {
				this.mesh.rotation.x = -Math.PI / 4;
				this.mesh.position.z -= 0.5;
			}
			if(o.side==='z') {
				this.mesh.rotation.x = Math.PI / 4;
				this.mesh.position.z += 0.5;
			}
        }
    }

    /* 在滑槽中的移动 */
    updateChute(delta){
        this.mesh.position.addScaledVector(this.dir, delta * speed*1.5);
        if(Math.abs(this.mesh.position.z) >= 5){
            scene.remove(this.mesh);
            return true;
        }
        return false;
    }

    update(delta){
        if(this.area === "import"){
            this.updateImport(delta);
        }else if(this.area === "tts"){
            this.updateTTS(delta);
        }else if(this.area === "chute"){
            return this.updateChute(delta);
        }
        return false;
    }
}

/* ---------- 动画 ---------- */
const parcels = new Map();
let paused=false, speed=1;
const clock = new THREE.Clock();
function animate(){
    requestAnimationFrame(animate);
    if(!paused){
        const delta = clock.getDelta();
        for(const [id,p] of parcels){
            const finished = p.update(delta);
            if(finished) parcels.delete(id);
        }
        document.getElementById('stats').textContent = `在线货物: ${parcels.size}`;
    }
    ctrl.update();
    renderer.render(scene, camera);
}
animate();

/* ---------- 按钮 / 包裹生成 ---------- */
document.getElementById('btnPlay').onclick = ()=>{
    paused=!paused;
    if(paused) clock.stop(); else clock.start();
    document.getElementById('btnPlay').textContent = paused ? '播放':'暂停';
};
document.getElementById('btnSpeed').onclick = ()=>{
	speed = speed === 1 ? 2 : speed === 2 ? 4 : speed === 4 ? 8 : speed === 8 ? 0.5 : 1;
    document.getElementById('btnSpeed').textContent = `x${speed}`;
};

let testId = 0;
setInterval(()=>{
    if(!paused && parcels.size < N*2){
        const impIdx = Math.floor(Math.random()*beltImp.length);
		// 导入口有等待则不生成新包裹
        for (const [id, parcel] of parcels) {
            if (parcel.impIdx === impIdx && parcel.area === 'import' && parcel.waiting) return;
        }
        parcels.set(testId, new Parcel(testId, Math.floor(Math.random()*chutes.length), impIdx));
        testId++;
    }
},1000);

/* ---------- 窗口适配 ---------- */
window.addEventListener('resize',()=>{
    camera.aspect = innerWidth/innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>